import com.aliasi.classify.Classification;
import com.aliasi.classify.Classified;
import com.aliasi.corpus.ObjectHandler;
import com.aliasi.features.Features;
import com.aliasi.matrix.EuclideanDistance;
import com.aliasi.matrix.Vector;
import com.aliasi.symbol.MapSymbolTable;
import com.aliasi.util.BoundedPriorityQueue;
import com.aliasi.util.Distance;
import com.aliasi.util.FeatureExtractor;
import com.aliasi.util.Pair;
import com.aliasi.util.Proximity;
import com.aliasi.util.ScoredObject;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class KnnNeighbours<E> implements ObjectHandler<Classified<E>> {   

    static final long serialVersionUID = 5692985587478284405L;

    final FeatureExtractor<? super E> mFeatureExtractor;

    protected final int mK;
    
    protected Proximity<Vector> mProximity;
    
    final boolean mWeightByProximity;
    
    protected final Map<Integer,List<Double>> mTrainingLatitude;
    
    protected final Map<Integer,List<Double>> mTrainingLongitude;
    
    protected final Map<Integer,List<Vector>> mTrainingVectors;
    
    protected final MapSymbolTable mFeatureSymbolTable;
    
    final MapSymbolTable mCategorySymbolTable;
    
    public KnnNeighbours(FeatureExtractor<? super E> featureExtractor, int k) {
        this(featureExtractor,k,EuclideanDistance.DISTANCE);
    }

    public KnnNeighbours(FeatureExtractor<? super E> featureExtractor, int k, Distance<Vector> distance) {
        this(featureExtractor,k,new ProximityWrapper(distance),false);
    }

    public KnnNeighbours(FeatureExtractor<? super E> extractor, int k, Proximity<Vector> proximity, boolean weightByProximity) {
        this(extractor,k,proximity,weightByProximity,
             new HashMap<Integer,List<Vector>>(),new HashMap<Integer,List<Double>>(),new HashMap<Integer,List<Double>>(),
             new MapSymbolTable(), new MapSymbolTable());
    }

    KnnNeighbours (FeatureExtractor<? super E> featureExtractor, int k, Proximity<Vector> proximity, boolean weightByProximity,
                  Map<Integer,List<Vector>> trainingVectors,
                  Map<Integer,List<Double>> trainingLatitude,
                  Map<Integer,List<Double>> trainingLongitude,
                  MapSymbolTable featureSymbolTable,
                  MapSymbolTable categorySymbolTable) {
        mFeatureExtractor = featureExtractor;
        mK = k;
        mProximity = proximity;
        mWeightByProximity = weightByProximity;
        mTrainingVectors = trainingVectors;
        mTrainingLatitude = trainingLatitude;
        mTrainingLongitude = trainingLongitude;
        mFeatureSymbolTable = featureSymbolTable;
        mCategorySymbolTable = categorySymbolTable;
    }

    /**
     * Returns the feature extractor for ths KNN classifier.
     *
     * @return The feature extractor.
     */
    public FeatureExtractor<? super E> featureExtractor() {
        return mFeatureExtractor;
    }

    /**
     * Returns the proximity measure for this KNN classifier.
     *
     * @return The proximity.
     */
    public Proximity<Vector> proximity() {
        return mProximity;
    }

    public List<String> categories ( ) {
        List<String> catList = new ArrayList<String>();
        for (Integer i : mTrainingVectors.keySet() )
            catList.add(mCategorySymbolTable.idToSymbol(i));
        return catList;
    }

    /**
     * Returns {@code true} if resposes are weighted by proximity.
     *
     * @return A flag indicating if this classifier is weighted
     * by proximity.
     */
    public boolean weightByProximity() {
        return mWeightByProximity;
    }

    /**
     * Returns the number K of neighbors used by this K-nearest
     * neighbors classifier.
     *
     * @return The K for this KNN classifier.
     */
    public int k() { return mK; }

    void handle(E trainingInstance, Classification classification, Double latitude, Double longitude) {
        String category = classification.bestCategory();
        Map<String,? extends Number> featureMap = mFeatureExtractor.features(trainingInstance);
        Vector vector
            = Features
            .toVectorAddSymbols(featureMap,
                                mFeatureSymbolTable,
                                Integer.MAX_VALUE-1,
                                false);
        Integer cat = mCategorySymbolTable.getOrAddSymbolInteger(category);
        List<Vector> aux = mTrainingVectors.get(cat);
        if (aux == null ) aux= new ArrayList<Vector>();
        aux.add(vector);
        mTrainingVectors.put(cat,aux);
        
        List<Double> aux2 = mTrainingLatitude.get(cat);
        if (aux2 == null ) aux2 = new ArrayList<Double>();
        aux2.add(latitude);
        mTrainingLatitude.put(cat,aux2);
        List<Double> aux3 = mTrainingLongitude.get(cat);
        if (aux3 == null ) aux3 = new ArrayList<Double>();
        aux3.add(longitude);
        mTrainingLongitude.put(cat,aux3);
    }

    /**
     * Handle the specified classified object as a training instance.
     * The training instance is converted to a feature vector using
     * the feature extractor, and then stored as a sparse vector
     * relative to a feature symbol table.
     *
     * @param classifiedObject Classified Object to use for training.
     */
    public void handle(Classified<E> classifiedObject, Double latitude, Double longitude) {
        handle(classifiedObject.getObject(),
               classifiedObject.getClassification(), latitude, longitude);
    }
    
    public MapSymbolTable getmCategorySymbolTable(){
    	return mCategorySymbolTable;
    }
    
    public BoundedPriorityQueue<ScoredObject<Pair<Double, Double>>> NearesNeighbours(E in, String cat) {
        Map<String,? extends Number> featureMap = featureExtractor().features(in);
        Vector inputVector
            = Features
            .toVector(featureMap,
                      mFeatureSymbolTable,
                      Integer.MAX_VALUE-1,
                      false);
        BoundedPriorityQueue<ScoredObject<Pair<Double, Double>>> queue = new BoundedPriorityQueue<ScoredObject<Pair<Double, Double>>>(ScoredObject.comparator(), mK);
        Integer category = mCategorySymbolTable.symbolToID(cat);
        List<Vector> examples = mTrainingVectors.get(category);
        List<Double> lats = mTrainingLatitude.get(category);
        List<Double> lons = mTrainingLongitude.get(category);
        
        for (int i = 0; i < examples.size(); i++) {
        		Vector trainingVector = examples.get(i);
        		Double lat = lats.get(i);
        		Double lon = lons.get(i);
        		Pair<Double,Double> pair = new Pair<Double,Double>(lat,lon);
        		double score = mProximity.proximity(inputVector,trainingVector);
        		if(Double.isNaN(score))
        			score = Double.MIN_VALUE;
        		queue.offer(new ScoredObject<Pair<Double, Double>>(pair,score));
        }
		return queue;
	}

    static class ProximityWrapper
        implements Proximity<Vector>, Serializable {

        static final long serialVersionUID = -1410999733708772109L;

        Distance<Vector> mDistance;

        public ProximityWrapper() { /* empty on purpose */ }

        public ProximityWrapper(Distance<Vector> distance) {
            mDistance = distance;
        }
        public double proximity(Vector v1, Vector v2) {
            double d = mDistance.distance(v1,v2);
            return (d < 0) ? Double.MAX_VALUE : (1.0/(1.0 + d));
        }
    }

	@Override
	public void handle(Classified<E> e) { throw new Error("ERROR"); }

}
